// (c) Copyright 2013-2014 Stephen Gold <sgold@sonic.net>
// Distributed under the terms of the GNU General Public License

/*
 This file is part of the Bats Game.

 The Bats Game is free software: you can redistribute it and/or modify
 it under the terms of the GNU General Public License as published by the
 Free Software Foundation, either version 3 of the License, or (at your
 option) any later version.

 The Bats Game is distributed in the hope that it will be useful, but
 WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
 or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
 for more details.

 You should have received a copy of the GNU General Public License
 along with the Bats Game.  If not, see <http://www.gnu.org/licenses/>.
 */
package bats.unit;

import bats.Assets;
import bats.Bats;
import bats.SpatialProperties;
import bats.control.BatsDelayControl;
import bats.control.DeleteControl;
import bats.control.PickedControl;
import bats.scene.SceneNode;
import com.jme3.animation.AnimChannel;
import com.jme3.animation.AnimControl;
import com.jme3.asset.AssetManager;
import com.jme3.bullet.control.KinematicRagdollControl;
import com.jme3.bullet.control.KinematicRagdollControl.Mode;
import com.jme3.math.FastMath;
import com.jme3.math.Quaternion;
import com.jme3.math.Vector3f;
import com.jme3.renderer.queue.RenderQueue.ShadowMode;
import com.jme3.scene.Node;
import com.jme3.scene.Spatial;
import java.util.ArrayList;
import java.util.Collection;
import java.util.logging.Level;
import java.util.logging.Logger;
import jme3utilities.MySkeleton;
import jme3utilities.MySpatial;
import jme3utilities.debug.SkeletonDebugControl;
import jme3utilities.math.VectorXZ;

/**
 * All the "sinbad" units in a particular scene.
 * <p>
 * A "sinbad" is a muscular, unarmored humanoid with a bulging brow, goatee,
 * protruding lower fangs, yellow earrings, and green skin. He wears a
 * headscarf, bracers, trousers, belt, suspenders, and boots.
 *
 * @author Stephen Gold <sgold@sonic.net>
 */
public class Sinbads
        extends UnitClass {
    // *************************************************************************
    // constants

    /**
     * hardness setting (in meters per second)
     */
    final private static float hardness = 0.1f;
    /**
     * when a sinbad is impaired, it will delete itself after this time interval
     * (in seconds)
     */
    final private static float impairTimeout = 5f;
    /**
     * number of geometries in the model
     */
    final private static int geometriesInModel = 7;
    /**
     * message logger for this class
     */
    final private static Logger logger =
            Logger.getLogger(Sinbads.class.getName());
    /**
     * asset path to the complete model
     */
    final private static String modelPath = "Models/Sinbad/Sinbad.mesh.xml";
    /**
     * pickType for sinbads
     */
    final private static String pickType = "sinbad";
    /**
     * model coordinates of a sinbad's pick indicator
     */
    final private static Vector3f indicatorOffset = new Vector3f(0f, -3f, 0f);
    /**
     * model coordinates of a sinbad's viewpoint
     */
    final private static Vector3f viewpointOffset =
            new Vector3f(0f, 3.3f, 1.2f);
    // *************************************************************************
    // constructors

    /**
     * Instantiate for the specified scene.
     *
     * @param scene where these sinbads reside (not null)
     */
    public Sinbads(SceneNode scene) {
        super(scene, pickType);

        setStandardMass(100f);
        setStandardScale(0.17f);
    }
    // *************************************************************************
    // UnitClass methods

    /**
     * Create a unit and add it to the scene and the pick, with its base at the
     * specified world coordinates.
     *
     * @param relativeSize the unit's size relative to the standard (&gt;0)
     * @param unused
     * @param baseLocation world coordinates of the unit's base (not null)
     * @param direction initial direction to face (unit vector)
     * @return a new parented node to represent the unit
     */
    @Override
    Node add(float relativeSize, String unused, Vector3f baseLocation,
            VectorXZ direction) {
        assert relativeSize > 0f : relativeSize;
        assert baseLocation != null;
        assert direction != null;
        assert direction.isUnitVector();
        /*
         * Load the "sinbad" model.
         */
        Node model = Assets.loadModel(modelPath, geometriesInModel);
        /*
         * Set game properties, name, and shadow mode.
         */
        SpatialProperties.setHardness(model, hardness);
        SpatialProperties.setPickType(model, pickType);
        model.setName(uniqueName());
        model.setShadowMode(ShadowMode.Cast);
        /*
         * Attach the main node as a pickable.
         */
        scene.getWorld().getPickables().attach(model);
        /*
         * Set the scale factor.
         */
        float scaleFactor = scene.getScaleFactor();
        float modelScale = standardScale * relativeSize / scaleFactor;
        model.setLocalScale(modelScale);
        assert getRelativeSize(model) == relativeSize : relativeSize;
        /*
         * Initialize the unit's direction.
         */
        float yRotateAngle = FastMath.HALF_PI - direction.azimuth();
        model.rotate(0f, yRotateAngle, 0f);
        /*
         * Initialize the unit's location.
         */
        Vector3f temp = new Vector3f(0f, -5f, 0f);
        temp.multLocal(-modelScale);
        Vector3f centerInWorld = baseLocation.add(temp);
        MySpatial.setWorldLocation(model, centerInWorld);
        /*
         * Add controls for various behaviors.
         */
        SinbadControl dollControl = new SinbadControl();
        model.addControl(dollControl);
        scene.getWorld().getPhysics().getSpace().add(dollControl);

        if (Bats.isSimulationMode()) {
            /*
             * Start animating idle loop.
             */
            AnimControl animControl = model.getControl(AnimControl.class);
            AnimChannel whole = animControl.createChannel();
            whole.setAnim("IdleBase");
        }
        /*
         * Add indicator controls.
         */
        PickedControl pickedControl = new PickedControl();
        model.addControl(pickedControl);
        pickedControl.setEnabled(true);
        TargetControl targetControl = new TargetControl();
        model.addControl(targetControl);
        ViewpointControl viewpointControl = new ViewpointControl();
        model.addControl(viewpointControl);
        /*
         * Add skeleton debug control.
         */
        AssetManager assetManager = Assets.getManager();
        SkeletonDebugControl skeletonDebugConrol =
                new SkeletonDebugControl(assetManager);
        model.addControl(skeletonDebugConrol);
        /*
         * Make sure all the necessary attachment nodes are in place.
         */
        MySkeleton.worldLocation(model, "Stomach");

        assert !MySpatial.isOrphan(model);
        return model;
    }

    /**
     * Get the world coordinates of a sinbad's pick indicator.
     *
     * @param spatial which sinbad to locate (not null)
     * @return a new vector
     */
    @Override
    public Vector3f getIndicatorLocation(Spatial spatial) {
        Vector3f result = spatial.localToWorld(indicatorOffset, null);
        return result;
    }

    /**
     * Get the local direction which a sinbad's pelvis faces.
     *
     * @param spatial which sinbad to orient (not null)
     * @return a new unit vector
     */
    @Override
    protected Vector3f getModelPelvicDirection(Spatial spatial) {
        assert spatial != null;

        return Vector3f.UNIT_Z.clone();
    }

    /**
     * Get the world locations of a sinbad's targets.
     *
     * @param spatial which sinbad to locate (not null)
     * @return a new list of non-null vectors
     */
    @Override
    public Collection<Vector3f> getTargets(Spatial spatial) {
        assert spatial != null;
        /*
         * just one target for now: the sinbad's stomach
         */
        ArrayList<Vector3f> result = new ArrayList<>();
        Vector3f location = MySkeleton.worldLocation(spatial, "Stomach");
        assert location != null;
        result.add(location);
        return result;
    }

    /**
     * Get the world coordinates of a sinbad's viewpoint.
     *
     * @param spatial which sinbad to locate (not null)
     * @return a new vector (not null)
     */
    @Override
    public Vector3f getViewpointLocation(Spatial spatial) {
        Vector3f result = spatial.localToWorld(viewpointOffset, null);
        return result;
    }

    /**
     * Get the world orientation of a sinbad's viewpoint.
     *
     * @param spatial which sinbad to orient (not null)
     * @return a new instance
     */
    @Override
    public Quaternion getViewpointOrientation(Spatial spatial) {
        assert spatial != null;

        Vector3f direction = getWorldPelvicDirection(spatial);
        Quaternion orientation = new Quaternion();
        orientation.lookAt(direction, Vector3f.UNIT_Y.clone());
        return orientation;
    }

    /**
     * Impair a sinbad.
     *
     * @param spatial which sinbad to impair (not null)
     */
    @Override
    public void impair(Spatial spatial) {
        assert spatial != null;

        if (isImpaired(spatial)) {
            /*
             * Once a sinbad is impaired, this method has no effect.
             */
            return;
        }
        if (SpatialProperties.isHidden(spatial)) {
            /*
             * As long as the sinbad is hidden, this method has no effect.
             */
            return;
        }
        logger.log(Level.INFO, "impairing {0}", spatial.getName());

        KinematicRagdollControl dollControl =
                spatial.getControl(KinematicRagdollControl.class);
        dollControl.setRagdollMode();
        /*
         * Un-pick the unit and make it unpickable.
         */
        SpatialProperties.setPicked(spatial, false);
        SpatialProperties.clearPickType(spatial);
        scene.getWorld().attach(spatial);

        AnimControl animControl = spatial.getControl(AnimControl.class);
        animControl.setEnabled(false);
        /*
         * The sinbad deletes itself after it's been impaired for some time.
         */
        DeleteControl deleteControl = new DeleteControl(scene);
        spatial.addControl(deleteControl);
        BatsDelayControl delayControl = new BatsDelayControl();
        spatial.addControl(delayControl);
        delayControl.addSlave(deleteControl);
        delayControl.setRemainingSeconds(impairTimeout);
        delayControl.setEnabled(true);
    }

    /**
     * Test whether the specified sinbad is impaired.
     *
     * @param spatial which sinbad (not null)
     * @return true if the sinbad is impaired; false if not a sinbad or not
     * impaired
     */
    @Override
    public boolean isImpaired(Spatial spatial) {
        KinematicRagdollControl dollControl =
                spatial.getControl(KinematicRagdollControl.class);
        Mode mode = dollControl.getMode();
        return mode == Mode.Ragdoll;
    }

    /**
     * Update a sinbad's collision shape.
     *
     * @param spatial which sinbad to update (not null)
     */
    @Override
    public void updateCollisionShape(Spatial spatial) {
    }
}
